import numpy as np
import pandas as pd
import matplotlib as mpl
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
import os
import warnings
import datetime
import missingno as msno
from tqdm import tqdm
warnings.filterwarnings('ignore')
mpl.rcParams['figure.figsize'] = (12,4)
mpl.rcParams['figure.dpi'] = 300
sns.set_style('darkgrid')
tqdm.pandas()
Você trabalha como Analista de Dados em uma empresa que fornece um serviço de rede social. O acesso ao serviço é concedido por meio de assinaturas mensais e os membros podem interagir por meio de comentários, respostas, adição de amigos e outras ações similares.
Alguns colaboradores perceberam uma alta evasão de membros da plataforma. Para validar tal hipótese, foi disponibilizado a você um conjunto de bases de dados que contempla informações sobre os assinantes e seus eventos na plataforma.
Você deve:
path_account = os.path.join('data','Dataset do Case de Analista_Cientista de Dados','account.csv')
path_event_type = os.path.join('data','Dataset do Case de Analista_Cientista de Dados','event_type.csv')
path_event = os.path.join('data','Dataset do Case de Analista_Cientista de Dados','event.csv')
path_subscription = os.path.join('data','Dataset do Case de Analista_Cientista de Dados','subscription.csv')
account = pd.read_csv(path_account)
event_type = pd.read_csv(path_event_type)
event = pd.read_csv(path_event)
subscription = pd.read_csv(path_subscription)

display(account.head()), account.info()
| id | channel | date_of_birth | country | |
|---|---|---|---|---|
| 0 | 1 | appstore2 | 1948-09-10 | CN |
| 1 | 2 | appstore1 | 1952-11-06 | AU |
| 2 | 3 | appstore1 | 2002-11-10 | GB |
| 3 | 4 | appstore1 | 1976-06-18 | US |
| 4 | 5 | appstore1 | 1975-11-18 | KR |
<class 'pandas.core.frame.DataFrame'> RangeIndex: 14641 entries, 0 to 14640 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 id 14641 non-null int64 1 channel 14641 non-null object 2 date_of_birth 14641 non-null object 3 country 13030 non-null object dtypes: int64(1), object(3) memory usage: 457.7+ KB
(None, None)
display(event_type.head()), account.info()
| event_type_id | event_type_name | |
|---|---|---|
| 0 | 0 | post |
| 1 | 1 | newfriend |
| 2 | 2 | like |
| 3 | 3 | adview |
| 4 | 4 | dislike |
<class 'pandas.core.frame.DataFrame'> RangeIndex: 14641 entries, 0 to 14640 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 id 14641 non-null int64 1 channel 14641 non-null object 2 date_of_birth 14641 non-null object 3 country 13030 non-null object dtypes: int64(1), object(3) memory usage: 457.7+ KB
(None, None)
display(event.head()), event.info()
| account_id | event_time | event_type_id | |
|---|---|---|---|
| 0 | 1 | 2020-01-06 03:20:00 | 0 |
| 1 | 1 | 2020-01-06 00:55:29 | 0 |
| 2 | 1 | 2020-01-06 10:16:34 | 1 |
| 3 | 1 | 2020-01-06 09:31:16 | 2 |
| 4 | 1 | 2020-01-06 20:40:21 | 2 |
<class 'pandas.core.frame.DataFrame'> RangeIndex: 17746587 entries, 0 to 17746586 Data columns (total 3 columns): # Column Dtype --- ------ ----- 0 account_id int64 1 event_time object 2 event_type_id int64 dtypes: int64(2), object(1) memory usage: 406.2+ MB
(None, None)
display(subscription.head()), subscription.info()
| id | account_id | product | start_date | end_date | mrr | |
|---|---|---|---|---|---|---|
| 0 | 0 | 1 | standard | 2020-01-06 | 2020-02-06 | 9.99 |
| 1 | 1 | 1 | standard | 2020-02-06 | 2020-03-06 | 9.99 |
| 2 | 2 | 1 | standard | 2020-03-06 | 2020-04-06 | 9.99 |
| 3 | 3 | 1 | standard | 2020-04-06 | 2020-05-06 | 9.99 |
| 4 | 4 | 1 | standard | 2020-05-06 | 2020-06-06 | 9.99 |
<class 'pandas.core.frame.DataFrame'> RangeIndex: 55502 entries, 0 to 55501 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 id 55502 non-null int64 1 account_id 55502 non-null int64 2 product 55502 non-null object 3 start_date 55502 non-null object 4 end_date 55502 non-null object 5 mrr 55502 non-null float64 dtypes: float64(1), int64(2), object(3) memory usage: 2.5+ MB
(None, None)
print('*'*5,'Table Shapes', '*'*5, '\n')
(print(f'Tabela account: \t{account.shape[1]} features, {account.shape[0]} registros'))
(print(f'Tabela event_type: \t{event_type.shape[1]} features, {event_type.shape[0]} registros'))
(print(f'Tabela event: \t\t{event.shape[1]} features, {event.shape[0]} registros'))
(print(f'Tabela subscription: \t{subscription.shape[1]} features, {subscription.shape[0]} registros'))
***** Table Shapes ***** Tabela account: 4 features, 14641 registros Tabela event_type: 2 features, 8 registros Tabela event: 3 features, 17746587 registros Tabela subscription: 6 features, 55502 registros
account.channel.unique()
array(['appstore2', 'appstore1', 'web'], dtype=object)
account.country.unique()
array(['CN', 'AU', 'GB', 'US', 'KR', 'IT', 'MX', 'CO', 'JP', nan, 'BR',
'DE', 'ES', 'RU', 'AR', 'NO', 'CH', 'SE', 'IN', 'CA', 'ID', 'FR',
'DK', 'GR', 'NZ', 'PT', 'NL'], dtype=object)
account.id.unique()
array([ 1, 2, 3, ..., 14639, 14640, 14641])
account['channel'] = account['channel'].astype('category')
account['country'] = account['country'].astype('category')
account['date_of_birth'] = pd.to_datetime(account['date_of_birth'], format='%Y-%m-%d')
account.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 14641 entries, 0 to 14640 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 id 14641 non-null int64 1 channel 14641 non-null category 2 date_of_birth 14641 non-null datetime64[ns] 3 country 13030 non-null category dtypes: category(2), datetime64[ns](1), int64(1) memory usage: 258.9 KB
event.columns
Index(['account_id', 'event_time', 'event_type_id'], dtype='object')
event.event_type_id.unique()
array([0, 1, 2, 3, 6, 7, 4, 5])
event.account_id.unique()
array([ 1, 2, 3, ..., 14639, 14640, 14641])
event['event_type_id'] = event['event_type_id'].astype('category')
event['event_time'] = pd.to_datetime(event['event_time'], format='%Y-%m-%d %H:%M:%S')
event['account_id'] = event['account_id'].astype('category')
event.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 17746587 entries, 0 to 17746586 Data columns (total 3 columns): # Column Dtype --- ------ ----- 0 account_id category 1 event_time datetime64[ns] 2 event_type_id category dtypes: category(2), datetime64[ns](1) memory usage: 186.8 MB
event_type.columns
Index(['event_type_id', 'event_type_name'], dtype='object')
event_type.event_type_id.unique()
array([0, 1, 2, 3, 4, 5, 6, 7])
event_type.event_type_name.unique()
array(['post', 'newfriend', 'like', 'adview', 'dislike', 'unfriend',
'message', 'reply'], dtype=object)
event_type.event_type_id = event_type.event_type_id.astype('category')
event_type.event_type_name = event_type.event_type_name.astype('category')
A tabela apresenta apenas valores únicos e de acordo com o tipo já atribuído, portanto, não há necessidade de conversão de tipos.
subscription.head()
| id | account_id | product | start_date | end_date | mrr | |
|---|---|---|---|---|---|---|
| 0 | 0 | 1 | standard | 2020-01-06 | 2020-02-06 | 9.99 |
| 1 | 1 | 1 | standard | 2020-02-06 | 2020-03-06 | 9.99 |
| 2 | 2 | 1 | standard | 2020-03-06 | 2020-04-06 | 9.99 |
| 3 | 3 | 1 | standard | 2020-04-06 | 2020-05-06 | 9.99 |
| 4 | 4 | 1 | standard | 2020-05-06 | 2020-06-06 | 9.99 |
subscription['product'].unique()
array(['standard'], dtype=object)
subscription['mrr'].unique()
array([9.99])
Há apenas um único tipo de produto vendido a um único preço.
subscription['account_id'].unique()
array([ 1, 2, 3, ..., 14639, 14640, 14641])
subscription['start_date'] = pd.to_datetime(subscription.start_date, format='%Y-%m-%d')
subscription['end_date'] = pd.to_datetime(subscription.end_date, format='%Y-%m-%d')
subscription['product'] = subscription['product'].astype('category')
subscription['account_id'] = subscription['account_id'].astype('category')
subscription.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 55502 entries, 0 to 55501 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 id 55502 non-null int64 1 account_id 55502 non-null category 2 product 55502 non-null category 3 start_date 55502 non-null datetime64[ns] 4 end_date 55502 non-null datetime64[ns] 5 mrr 55502 non-null float64 dtypes: category(2), datetime64[ns](2), float64(1), int64(1) memory usage: 2.5 MB
account.isna().sum()
id 0 channel 0 date_of_birth 0 country 1611 dtype: int64
event_type.isna().sum()
event_type_id 0 event_type_name 0 dtype: int64
event.isna().sum()
account_id 0 event_time 0 event_type_id 0 dtype: int64
subscription.isna().sum()
id 0 account_id 0 product 0 start_date 0 end_date 0 mrr 0 dtype: int64
Apenas a tabela account apresenta valores faltantes. São 1611 valores faltantes na coluna do país de origem dos clientes.
(account.country.value_counts())
CN 2241 US 2153 JP 740 BR 725 IN 631 CA 613 GB 595 DE 592 AU 576 MX 523 KR 450 AR 430 FR 417 NZ 318 CO 288 RU 270 ID 159 DK 158 NL 155 ES 153 GR 150 NO 149 IT 143 CH 137 SE 137 PT 127 Name: country, dtype: int64
msno.matrix(account, figsize = (16,5))
<AxesSubplot:>
Como são muitos valores únicos na feature e os valores faltantes estão uniformemente distribuídos no dataset, não é possível fazer alguma dedução segura para uma imputação imediata. O tratamento será feito de acordo com a situação que a variável for trabalhada.
print(f'Duplicatas em Event: {event.duplicated().sum()}')
print(f'Duplicatas em Account: {account.duplicated().sum()}')
print(f'Duplicatas em Subscription: {subscription.duplicated().sum()}')
print(f'Duplicatas em Event_Type: {event_type.duplicated().sum()}')
Duplicatas em Event: 1296 Duplicatas em Account: 0 Duplicatas em Subscription: 0 Duplicatas em Event_Type: 0
event.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 17746587 entries, 0 to 17746586 Data columns (total 3 columns): # Column Dtype --- ------ ----- 0 account_id category 1 event_time datetime64[ns] 2 event_type_id category dtypes: category(2), datetime64[ns](1) memory usage: 186.8 MB
Considerando que a coluna event_time da tabela event dá a precisão em segundos da ocorrência do evento, pode-se considerar as linhas duplicadas nesta tabela como realmente um registro duplo da mesma ocorrência. Sendo assim, portanto, é razoável que sejam retiradas da análise
event.drop_duplicates(inplace = True)
# Preservar RangeIndex para redução do uso de memória
event.reset_index(drop = True, inplace = True)
print(f'Duplicatas em Event: {event.duplicated().sum()}')
print(f'Duplicatas em Account: {account.duplicated().sum()}')
print(f'Duplicatas em Subscription: {subscription.duplicated().sum()}')
print(f'Duplicatas em Event_Type: {event_type.duplicated().sum()}')
Duplicatas em Event: 0 Duplicatas em Account: 0 Duplicatas em Subscription: 0 Duplicatas em Event_Type: 0
event.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 17745291 entries, 0 to 17745290 Data columns (total 3 columns): # Column Dtype --- ------ ----- 0 account_id category 1 event_time datetime64[ns] 2 event_type_id category dtypes: category(2), datetime64[ns](1) memory usage: 186.8 MB
event.head()
| account_id | event_time | event_type_id | |
|---|---|---|---|
| 0 | 1 | 2020-01-06 03:20:00 | 0 |
| 1 | 1 | 2020-01-06 00:55:29 | 0 |
| 2 | 1 | 2020-01-06 10:16:34 | 1 |
| 3 | 1 | 2020-01-06 09:31:16 | 2 |
| 4 | 1 | 2020-01-06 20:40:21 | 2 |
Questões levantadas:
print(f'Início: {event.event_time.min()} \nFim: \t{event.event_time.max()}')
Início: 2020-01-01 00:00:01 Fim: 2020-06-30 23:59:59
O período analisado ocorre do início do ano de 2020, em janeiro até o término do mês de junho.
meses = dict(zip(np.arange(1,7), ['Jan','Fev','Mar','Abr','Mai','Jun']))
account_subscription = account.merge(subscription, how = 'left', left_on = 'id', right_on = 'account_id').drop(['id_y', 'id_x'], axis = 1)
account_subscription['start_month'] = account_subscription['start_date'].dt.month
account_subscription['start_day'] = account_subscription['start_date'].dt.day
account_subscription['end_month'] = account_subscription['end_date'].dt.month
account_subscription['end_day'] = account_subscription['end_date'].dt.month
((account_subscription.end_date - account_subscription.start_date).value_counts())
31 days 32549 30 days 12305 29 days 10648 dtype: int64
Todas as assinaturas são de duração de 1 mês.
account_subscription.head()
| channel | date_of_birth | country | account_id | product | start_date | end_date | mrr | start_month | start_day | end_month | end_day | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | appstore2 | 1948-09-10 | CN | 1 | standard | 2020-01-06 | 2020-02-06 | 9.99 | 1 | 6 | 2 | 2 |
| 1 | appstore2 | 1948-09-10 | CN | 1 | standard | 2020-02-06 | 2020-03-06 | 9.99 | 2 | 6 | 3 | 3 |
| 2 | appstore2 | 1948-09-10 | CN | 1 | standard | 2020-03-06 | 2020-04-06 | 9.99 | 3 | 6 | 4 | 4 |
| 3 | appstore2 | 1948-09-10 | CN | 1 | standard | 2020-04-06 | 2020-05-06 | 9.99 | 4 | 6 | 5 | 5 |
| 4 | appstore2 | 1948-09-10 | CN | 1 | standard | 2020-05-06 | 2020-06-06 | 9.99 | 5 | 6 | 6 | 6 |
sig_month = account_subscription.groupby('start_month').count()['channel']
sns.lineplot(x = sig_month.index,
y = sig_month.values,
palette = 'Set1',
marker = 'o',
color = '#BA0000',
linestyle = '--')
plt.xlabel('Mês', fontsize = 12)
plt.ylabel('Assinaturas', fontsize = 12)
plt.xticks(np.arange(1,7), meses.values())
plt.title('Assinaturas realizadas ao mês', fontdict = {'fontsize':14, 'weight':'bold'})
plt.savefig('img/sign_month.png')
plt.show()
O serviço vinha numa tendência crescente de janeiro até maio. No entanto, há uma forte queda no número de assinaturas registradas no mês de junho.
event['month'] = event.event_time.dt.month
event.month = event.month.astype('category')
month_groupby = event.groupby('month')
sns.lineplot(data = event.groupby(['account_id','month']).count().reset_index(),
x = 'month', y = 'event_time',
marker = 'o',
linewidth = 2,
linestyle = '--')
plt.xticks(ticks = np.arange(1,7), labels = ['Jan','Fev','Mar','Abr','Mai','Jun'])
plt.ylabel('Interações por usuário', fontsize = 12)
plt.xlabel('Mês', fontsize = 12)
plt.title('Média de interações por usuários por mês', fontdict = {'fontsize':14, 'weight':'bold'})
plt.tight_layout()
plt.savefig('img/int_month.png')
plt.show()
grouped_interactions = event.groupby('event_type_id')
for interaction, group in grouped_interactions:
ax = sns.lineplot(x = group.month.value_counts().index,
y = group.month.value_counts().values,
label = interaction,
marker = '^',
linewidth = 2,
linestyle = '--')
plt.title('Número de interações por mês por categoria', weight = 'bold', fontsize = 14)
ax.set_ylabel('Interações',fontsize=12)
ax.set_xlabel('Mês',fontsize=12)
ax.legend(event_type.event_type_name.unique(), title = 'Interação')
ax.set_xticks(np.arange(1,7), meses.values())
plt.tight_layout()
plt.show()
A proporção entre as diferentes formas de interação possíveis permaneceu relativamente constante durante os meses, o que indica que o comportamento dos usuários não variou durante o período, mesmo no momento em que pararam de renovar a assinatura na rede.
# Coluna que define se assinatura é nova ou renovada
account_subscription['renew'] = account_subscription.progress_apply(lambda x: x.account_id in (account_subscription[account_subscription.start_month == (x.start_month-1)]['account_id']).values, axis = 1)
100%|████████████████████████████████████| 55502/55502 [02:51<00:00, 323.74it/s]
account_subscription.sort_values(by='account_id').head()
| channel | date_of_birth | country | account_id | product | start_date | end_date | mrr | start_month | start_day | end_month | end_day | renew | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | appstore2 | 1948-09-10 | CN | 1 | standard | 2020-01-06 | 2020-02-06 | 9.99 | 1 | 6 | 2 | 2 | False |
| 1 | appstore2 | 1948-09-10 | CN | 1 | standard | 2020-02-06 | 2020-03-06 | 9.99 | 2 | 6 | 3 | 3 | True |
| 2 | appstore2 | 1948-09-10 | CN | 1 | standard | 2020-03-06 | 2020-04-06 | 9.99 | 3 | 6 | 4 | 4 | True |
| 3 | appstore2 | 1948-09-10 | CN | 1 | standard | 2020-04-06 | 2020-05-06 | 9.99 | 4 | 6 | 5 | 5 | True |
| 4 | appstore2 | 1948-09-10 | CN | 1 | standard | 2020-05-06 | 2020-06-06 | 9.99 | 5 | 6 | 6 | 6 | True |
group_channels = account_subscription.groupby(['start_month','channel']).count()['product'].reset_index()
sns.countplot(data = account_subscription,
x = 'start_month',
hue = 'renew')
plt.legend(labels = ['Nova Assinatura','Renovação'], title = 'Evento')
plt.xticks(ticks = np.arange(0,6), labels=['Jan','Fev','Mar','Abr','Mai','Jun'])
plt.xlabel('Mês', fontsize = 12)
plt.ylabel('Assinaturas', fontsize = 12)
plt.title('Novas Assinaturas x Renovações',fontsize = 14, weight = 'bold')
plt.savefig('img/renews.png')
plt.show()
sns.lineplot(data = account_subscription.groupby(['start_month','channel']).count()['product'].reset_index(),
x ='start_month',
y ='product',
hue = 'channel',
marker = 'o')
plt.title('Assinaturas por canal', weight = 'bold', fontsize = 14)
plt.xlabel('Mês', fontsize = 12)
plt.ylabel('Assinaturas', fontsize = 12)
plt.xticks(np.arange(1,7), meses.values())
plt.legend(title = 'Canal de Assinatura')
plt.savefig('img/channels.png')
plt.show()
account_subscription['age'] = account_subscription.date_of_birth.progress_apply(lambda x: datetime.date.today() - x.date())
100%|█████████████████████████████████| 55502/55502 [00:00<00:00, 129353.47it/s]
account_subscription['age'] = round(account_subscription.age.dt.days/365,0)
account_subscription.age.min(), account_subscription.age.max()
(14.0, 85.0)
sns.histplot(account_subscription.age)
plt.title('Distribuição de assinantes por idade', fontdict = {'fontsize':14, 'weight':'bold'})
plt.xlabel('Idade', fontsize = 12)
plt.ylabel('Assinantes', fontsize = 12)
plt.show()
O gráfico de idades mostra que há uma distribuição picos em diferentes faixas de idade. Para facilitar a análise, será feito o agrupamento das idades em faixas etárias.
Considerando:
account_subscription['age_group'] = pd.cut(account_subscription.age, bins=[0,19,39,59,79, 100], labels=['Adolescentes e Jovens','Jovens Adultos','Adultos','Idosos', 'Idosos+'])
colors = sns.color_palette('pastel')[0:5]
plt.figure(figsize = (4,2))
explode = [0.01, 0.01, 0.01, 0.01, 0.01]
plt.pie(account_subscription.drop_duplicates(['account_id'])['age_group'].value_counts().values,
labels = account_subscription.drop_duplicates(['account_id'])['age_group'].value_counts().index,
colors = colors,
autopct ='%.0f%%',
explode = explode,
textprops = {'fontsize': 5})
plt.title('Distribuição do Público por Faixa Etária',fontsize= 8, weight = 'bold')
plt.savefig('img/age_groups.png')
plt.show()
Principais públicos dividem-se igualmente entre jovens adultos, adultos e idosos, chegando a somar aprox. 85% do público total.
ltv = subscription.groupby('account_id').agg({'start_date':'min', 'end_date':'max'})
ltv['period'] = (ltv.progress_apply(lambda x: int((x.end_date - x.start_date).days/30), axis = 1)).astype('category')
100%|███████████████████████████████████| 14641/14641 [00:04<00:00, 3623.36it/s]
sns.histplot(data=ltv, x='period')
plt.title('Distribuição do período de assinatura por cliente', fontdict = {'fontsize':14, 'weight':'bold'})
plt.xlabel('Tempo de assinatura (meses)', fontsize = 12)
plt.ylabel('Assinantes', fontsize = 12)
plt.savefig('img/sign_period.png')
plt.show()
A maior parte dos clientes assinou o serviço durante 5 dos 6 meses avaliados. O fato pode indicar que há algum nível de fidelidade do cliente com a rede.
ltv['age_group'] = (ltv.index.map(dict(zip(account_subscription.account_id,account_subscription.age_group)))).astype('category')
age_hue_order = ['Adolescentes e Jovens','Jovens Adultos','Adultos','Idosos','Idosos+']
sns.countplot(data = ltv,
x = 'period',
hue = 'age_group',
hue_order = age_hue_order)
plt.xlabel('Tempo de assinatura (meses)', fontsize = 12)
plt.ylabel('Assinantes', fontsize = 12)
plt.title('Período de assinatura em cada faixa etária', fontdict = {'fontsize':14, 'weight':'bold'})
plt.legend(title = 'Faixa Etária')
plt.show()
Não parece haver variação significativa na proporção de assinantes por grupo etário considerando todos os períodos de assiantura.
ltv_event = ltv.merge(event, how='right',
left_index = True,
right_on = 'account_id')
interaction_hue_order = [2,6,0,3,7,4,1,5]
interaction_legend = ['like','message','post','adview','reply','dislike','newfriend','unfriend']
sns.countplot(data = ltv_event[['age_group','event_type_id']],
x = 'age_group',
hue = 'event_type_id',
hue_order = interaction_hue_order)
plt.xlabel('Faixa Etária', fontsize=12)
plt.ylabel('Interações', fontsize=12)
plt.title('Tipos de interações registradas entre faixas etárias', fontdict = {'fontsize':14, 'weight':'bold'})
plt.legend(interaction_legend, title = 'Interação')
plt.show()
A proporção de interações praticadas pelos usuários de cada grupo etário parece não variar, indicando que os usuários parecem interagir com a plataforma de maneira semelhante, independente da sua idade.
temp = account.country.value_counts().sort_values(ascending = False)
sns.barplot(x = temp.index,
y = temp,
order = temp.index)
plt.xlabel('País', fontsize=12)
plt.ylabel('Assinantes', fontsize=12)
plt.title('Volume de assinaturas por país no período', fontdict = {'fontsize':14, 'weight':'bold'})
plt.tight_layout()
plt.show()
China e Estados Unidos constituem a maior fração do público assinante do serviço.
grouped_countries = [account_subscription[account_subscription.country==ctr] for ctr in account.country.unique() if ctr is not np.nan]
#Cores para a as séries temporais de cada país no gráfico abaixo
colors = ['#800000', '#FF4500','#CD5C5C','#FFD700', '#B8860B',
'#EEE8AA', '#9ACD32','#7CFC00','#006400','#8B4513',
'#000000', '#8FBC8F', '#00FA9A','#2E8B57','#2F4F4F',
'#008080','#00FFFF','#AFEEEE','#4682B4','#191970',
'#0000FF','#8A2BE2','#4B0082', '#8B008B','#FF00FF','#F5DEB3']
for n, country in tqdm(enumerate(grouped_countries)):
cur_country = country.groupby('start_month').count()['country']
ax = sns.lineplot(x = cur_country.index,
y = cur_country,
marker = '*',
label = country.country.iloc[0],
color = colors[n],
linestyle = '-')
handles, labels = ax.axes.get_legend_handles_labels()
ax.legend_.remove()
ax.figure.legend(handles, labels, ncol=7, loc='upper left',
bbox_to_anchor=(0.5, 1.15), frameon=True, title = 'País')
ax.set_xlabel('Mês', fontsize = 12)
ax.set_ylabel('Assinaturas', fontsize = 12)
ax.set_xticks(np.arange(1,7), labels = ['Jan','Fev','Mar','Abr','Mai','Jun'])
plt.title('Variação de assinaturas por país', loc = 'left', fontdict = {'fontsize':14, 'weight':'bold'})
plt.tight_layout()
plt.savefig('img/sign_month_countries.png')
plt.show()
26it [00:01, 23.66it/s]
Todos os países seguiram a mesma tendência e tiveram reduzido o número de assinantes da rede no mês de junho, o que indica que o churn observado foi um fenômeno mundial.
event_ts = event.set_index('event_time').resample('D').count()
subscription_ts = subscription.set_index('start_date').resample('D').count()
subscription_ts.loc['2020-06-02',:] = [0,0,0,0,0]
subscription_ts.loc['2020-06-30',:] = [0,0,0,0,0]
f, ax = plt.subplots(nrows = 2, sharex = True)
ax[0] = event_ts['account_id'].plot(linewidth=2, ax = ax[0])
ax[0].set_ylabel('Interações', fontsize = 12)
ax[0].set_xlabel('Mês', fontsize = 12)
ax[0].set_title('Interações diárias', fontsize = 14, weight = 'bold')
ax[1] = subscription_ts['account_id'].plot(linewidth=2, ax = ax[1])
ax[1].set_ylabel('Assinaturas', fontsize = 12)
ax[1].set_xlabel('Mês', fontsize = 12)
ax[1].set_title('Assinaturas diárias', fontsize = 14, weight = 'bold')
ax[1].set_xlim(['2020-01-01','2020-06-30'])
plt.tight_layout()
ax[0].axvline(x='2020-05-31', color = 'black', linestyle = '--')
ax[0].axvline(x='2020-04-30', color = 'black', linestyle = '--')
ax[0].axvline(x='2020-03-31', color = 'black', linestyle = '--')
ax[0].axvline(x='2020-02-29', color = 'black', linestyle = '--')
ax[0].axvline(x='2020-01-31', color = 'black', linestyle = '--')
ax[1].axvline(x='2020-04-30', color = 'black', linestyle = '--')
ax[1].axvline(x='2020-03-31', color = 'black', linestyle = '--')
ax[1].axvline(x='2020-02-29', color = 'black', linestyle = '--')
ax[1].axvline(x='2020-01-31', color = 'black', linestyle = '--')
ax[1].axvline(x='2020-05-31', color = 'black', linestyle = '--')
plt.savefig('img/daily_int_sign.png')
plt.show()
event_ts['dow'] = event_ts.index.dayofweek
dow = ['dom','seg','ter','qua','qui','sex','sab']
dow_events =event_ts.groupby('dow').mean()['account_id']
dow_events.index = dow
dow_events = dow_events.sort_values(ascending = False)
sns.barplot(y=dow_events.index, x = dow_events.values, orient = 'h')
plt.title('Média de interações por dia da semana', weight = 'bold', fontsize = 14)
plt.xlabel('Interações', fontsize = 12)
plt.ylabel('Dias da semana', fontsize = 12)
plt.savefig('img/dow_int.png')
plt.show()
Os dias com maior concentração de interações na plataforma são quinta-feira, sexta-feira e sábado. O resultado faz sentido já que a maioria do público da rede é adulto e provavelmente tem mais tempo de interagir com o conteúdo da rede nos fins de semana.
hour_event = event[['account_id','event_time']].set_index('event_time').resample('H').count()
hour_event['hour'] = hour_event.index.hour
(hour_event.groupby('hour').mean()['account_id']).plot(kind='line', marker = 'd', color = '#006400')
plt.xlim([0,23])
plt.xticks(np.arange(0,24,4))
plt.title('Média de interações por faixa horária', weight = 'bold', fontsize = 14)
plt.xlabel('Hora', fontsize = 12)
plt.ylabel('Interações', fontsize = 12)
plt.axvline(x=6, linestyle = '--', color = 'black')
plt.axvline(x=12, linestyle = '--', color = 'black')
plt.axvline(x=18, linestyle = '--', color = 'black')
plt.text(x=1.75,y=4070, s='Madrugada', fontdict={'weight':'bold'})
plt.text(x=8.25,y=4070, s='Manhã', fontdict={'weight':'bold'})
plt.text(x=14.5,y=4070, s='Tarde', fontdict={'weight':'bold'})
plt.text(x=20,y=4070, s='Noite', fontdict={'weight':'bold'})
plt.savefig('img/hour_int.png')
plt.show()
print('O gráfico mostra que o período de pico de interações com maior estabilidade ocorre durante a tarde, das 12:00 às 18:00.')
print(f"No entanto, a média de interações por horário, em geral, é relativamente constante ficando entre {round(hour_event.groupby('hour').mean()['account_id'].min(),0)} e {round(hour_event.groupby('hour').mean()['account_id'].max(),0)} interações por hora.")
O gráfico mostra que o período de pico de interações com maior estabilidade ocorre durante a tarde, das 12:00 às 18:00. No entanto, a média de interações por horário, em geral, é relativamente constante ficando entre 4057.0 e 4071.0 interações por hora.
age_country = account_subscription.groupby(['country','age_group'])['age'].count().reset_index()
sns.barplot(data = age_country,
x = 'country',
y = 'age',
hue = 'age_group')
plt.xlabel('País', fontsize = 12)
plt.ylabel('Assinantes', fontsize = 12)
plt.title('Faixa etária de assinantes por país', fontsize = 14, weight = 'bold')
plt.legend(title = 'Faixa Etária')
plt.show()
Não há variação nas proporções de grupos etários que assinam o serviço em cada país.
channel_country = account_subscription.groupby(['country','channel'])['age'].count().reset_index()
sns.barplot(data = channel_country,
x = 'country',
y = 'age',
hue = 'channel')
plt.xlabel('País', fontsize = 12)
plt.ylabel('Assinantes', fontsize = 12)
plt.title('Canais de assinatura utilizados por país', fontsize = 14, weight = 'bold')
plt.legend(title = 'Canal de Assinatura')
plt.show()
A escala preferência pelos canais de assinatura são as mesmas em todos os países onde o serviço é disponibilizado.
channel_age = account_subscription.groupby(['age_group','channel'])['age'].count().reset_index()
sns.barplot(data = channel_age,
x = 'age_group',
y = 'age',
hue = 'channel')
plt.xlabel('Faixa Etária', fontsize = 12)
plt.ylabel('Assinantes', fontsize = 12)
plt.title('Canais de assinatura utilizados por faixa etária', fontsize = 14, weight = 'bold')
plt.legend(title = 'Canal de Assinatura')
plt.show()
A preferência entre os canais de assinatura também é a mesma para todos os grupos etários.
clients_july = account_subscription[account_subscription['end_month'] == 7]
(clients_july.channel.value_counts()).plot(kind='barh')
plt.title('Canais de assinatura (assinantes julho)', weight = 'bold', fontsize = 12)
plt.xlabel('Assinantes',fontsize = 12)
plt.ylabel('Canais de assinatura',fontsize = 12)
plt.show()
(clients_july.age_group.value_counts()).plot(kind='barh')
plt.title('Faixa etária (assinantes julho)', weight = 'bold', fontsize = 12)
plt.xlabel('Assinantes',fontsize = 12)
plt.ylabel('Faixa etária',fontsize = 12)
plt.show()
event_clients_july = event[event.account_id.isin(clients_july.account_id.values)]
ecj_groupby = (event_clients_july.groupby(['month','event_type_id']).count()).reset_index()
sns.lineplot(data = ecj_groupby,
x = 'month',
y = 'account_id',
hue = 'event_type_id',
marker = 'o')
plt.legend(event_type.event_type_name.unique().tolist(), loc='upper left')
plt.title('Histórico de interações (assinantes julho)', weight = 'bold', fontsize = 12)
plt.xlabel('Mês',fontsize = 12)
plt.ylabel('Assinantes',fontsize = 12)
plt.xticks(np.arange(1,7), meses.values())
plt.show()
Em geral, o perfil e o comportamento dos usuários que deixaram de assinar a rede em junho é o mesmo que vinha já sendo observado nas análises mais gerais de perfil e comportamento.
ax = ltv.loc[clients_july.account_id.values,:].period.value_counts().plot(kind='bar')
ax.bar_label(ax.containers[0])
plt.title('Período de assinatura total (clientes julho)', weight = 'bold', fontsize = 12)
plt.xlabel('Período (meses)',fontsize = 12)
plt.ylabel('Assinantes',fontsize = 12)
plt.xticks(rotation = 0)
plt.show()
A maior parte dos assinantes do mês de junho/julho são utilizam a plataforma há, pelo menos, 6 meses.
Não há evidências fortes, pelos dados fornecidos, que os clientes que ainda renovaram sua assinatura para o mês de junho até julho sejam diferentes dos demais clientes que não renovaram suas assinaturas.
sub1 = subscription.sort_values(by=['account_id','start_date'])
sub1['next_'] = sub1.account_id.shift(-1)
sub1['churn'] = sub1.account_id != sub1.next_
sub1.churn = sub1.churn.astype('int').astype('category')
sub1['month'] = sub1.start_date.dt.month
sub1 = sub1[['account_id','month','churn']]
sub1.head()
| account_id | month | churn | |
|---|---|---|---|
| 0 | 1 | 1 | 0 |
| 1 | 1 | 2 | 0 |
| 2 | 1 | 3 | 0 |
| 3 | 1 | 4 | 0 |
| 4 | 1 | 5 | 1 |
pct = sub1.groupby(['month','churn']).count().reset_index()
no_churn_pct = np.around((pct[pct.churn==0]['account_id'].values/(pct[pct.churn==1]['account_id'].values + pct[pct.churn==0]['account_id'].values))*100,2)
churn_pct = np.around((pct[pct.churn==1]['account_id'].values/(pct[pct.churn==1]['account_id'].values + pct[pct.churn==0]['account_id'].values))*100,2)
ax = sns.countplot(data = sub1,
x = 'month',
hue = 'churn')
plt.title('Churn de assinantes por mês', weight = 'bold', fontsize = 12)
plt.xlabel('Mês',fontsize = 12)
plt.ylabel('Assinantes',fontsize = 12)
ax.set_xticks(np.arange(0,6), labels = ['Jan','Fev','Mar','Abr','Mai','Jun'])
plt.legend(['Não','Sim'], title = 'Churn', loc = 'upper right')
ax.bar_label(ax.containers[0], labels = no_churn_pct)
ax.bar_label(ax.containers[1], labels = churn_pct)
plt.savefig('img/churn.png')
plt.show()
A plataforma possui apenas um produto, com um único período de serviço de assinatura e preço único. Ações como a adição de novos planos personalizados ao gosto do cliente, planos de assinatura mais longos (trimestrais, semestrais, anuais) ou planos familiares a preços com desconto, disponibilização de conteúdo exclusivo para modalidades de assinatura mais completas, diferentes produtos com uma ampla faixa de preços podem aumentar o interesse no uso da plataforma. Ainda, a empresa pode oferecer um período gratuito de acesso ao conteúdo para que novos clientes possam testar e avaliar o produto.
A maior parte do público assinante está na fase adulta. Por isso, a comunicação, aparência e conteúdos trazidos para a plataforma devem dialogar com esse público para consigam se identificar e estabelecer uma relação com o cliente, além de atrair com consistência o seu público alvo, que aprecia a proposta de valor do produto e tem menor probabilidade de deixar de consumi-lo. Outro fator observado nos dados é a preferência de grande parte dos assinantes pela utilização da appstore2 como canal de assinatura. Essa preferência revelar importantes características do perfil socioeconômico dos usuários da plataforma que também podem servir de apoio na elaboração de planos e serviços para retenção de clientes.
A queda simultânea nas assinaturas e interações em diversos países, sendo Estados Unidos e China os principais, pode indicar algum tipo de falha na manutenção dos serviços da plataforma que impediram que os assinantes continuassem utilizando a rede e suas assinaturas. Outra possibilidade é que nesse período tenha sido lançado no mercado um produto concorrente com características semelhantes e maior alinhamento com as necessidades dos clientes, que tenha puxado para si os clientes da plataforma. No entanto, a maior parte dos assinantes já vinha consumindo o produto desde o primeiro mês do período analisado, o que mostra um nível de fidelidade à plataforma. Nesse caso, a alta evasão seria um alerta para a necessidade de aprimorar a fidelização dos clientes com a plataforma.
A maior parte das atividades na plataforma parece ocorrer na véspera dos finais de semana, com um pico maior de interações na parte da noite. A plataforma pode aproveitar esses períodos para lançar novos eventos exclusivos na plataforma, buscando manter o assinante engajado.
A plataforma poderia iniciar um trabalho de entrar em contato com os clientes que não renovaram no último mês para levantar informações acerca do motivo para enriquecer a base de de dados e melhorar o serviço e a previsibilidade de churn.